package com.noahsloan.nutils;

/**
 * Represents a bounded range of positive long integers. The bound (edge) can be
 * specified. If the start of the range is greater than the end of the range,
 * then a number greater than start is considered to precede 0.
 * 
 * @author noah
 * 
 */
public class Range {

    /**
     * The first number in the range. If start &gt; end, then the smallest
     * number in the range is 0 instead of start.
     */
    public final long start;

    /**
     * The last number in the range. May be less than start, in which case the
     * number following edge is 0.
     */
    public final long end;

    /**
     * The largest number in the Range. If start &lt; end, this is equal to end.
     */
    public final long edge;

    /**
     * Creates a Range from start to end. If end is before start, the largest
     * number in the Range is {@link Long#MAX_VALUE-1} and the smallest number
     * is 0.
     * 
     * @param start
     *            a positive long integer.
     * @param end
     *            a long integer less that {@link Long#MAX_VALUE-1}
     */
    public Range(final long start, final long end) {
        // if we let edge be MAX_VALUE, we get wrapping problems
        this(start, end, Math.min(Long.MAX_VALUE - 1, end));
    }

    /**
     * 
     * @param start
     * @param end
     * @param edge
     *            the highest number in the range
     */
    public Range(final long start, final long end, final long edge) {
        if (start < 0 || end < 0 || edge < 0) {
            throw new IllegalArgumentException(String.format(
                    "Ranges may only cover positive numbers. (%d,%d,%d)",
                    start, end, edge));
        }
        this.start = start;
        this.end = end;
        this.edge = edge;
    }

    /**
     * Does this range include <code>l</code>?
     * 
     * @param l
     * @return true if l is in the given range.
     */
    public boolean includes(long l) {
        if (l > edge) {
            return false;
        }
        if (end < start) {
            // it wraps around
            return l >= start || l <= end;
        } else {
            return l >= start && l <= end;
        }
    }

    /**
     * 
     * @param l
     * @return the distance from l to the end of the range
     */
    public long distance(long l) {
        return distance(l, end);
    }

    /**
     * Gets the distance from l1 to l2. e.g. range=9..5, wrapping at 12 (i.e. 12 =
     * 0) , the distance from 10 to 6 is 8 and the distance from 5 to 11 is -6.
     * 
     * @param one
     *            the starting point
     * @param two
     *            the ending point
     * @return the distance along this wrapping range from one to two. If two
     *         occurs in the range before one, then this number will be
     *         negative.
     */
    public long distance(final long one, final long two) {
        if (!includes(one) || !includes(two)) {
            throw new IllegalArgumentException(String.format(
                    "%d and/or %d not in %s", one, two, this));
        }
        if (end < start) {
            // 0..end..start..edge
            if (one >= start) {
                // 0..end..start..l1..edge
                if (two >= start) {
                    // 0..end..start..l?..l?..edge
                    return (two - one);
                } else {
                    // 0..l2..end..start..l1..edge
                    return edge - one + two + 1;
                }
            } else {
                // 0..l1..end..start..edge
                if (two >= start) {
                    // 0..l1..end..start..l2..edge
                    return -(edge - two + one + 1);
                } else {
                    // 0..l?..l?..end..start..edge
                    return two - one;
                }
            }
        } else {
            // start..l?..l?..end
            return two - one;
        }
    }

    /**
     * 
     * @param one
     * @param two
     * @return the number that occurs earlier in the range
     */
    public long min(final long one, final long two) {
        // if distance < 0, then one > two, otherwise either two is
        // greater or
        // they're equal, so return one
        return distance(one, two) < 0 ? two : one;
    }

    /**
     * 
     * @param one
     * @param two
     * @return the number that occurs later in the range
     */
    public long max(final long one, final long two) {
        return distance(one, two) < 0 ? one : two;
    }

    /**
     * 
     * @return this distance from start to finish. Note that this width is not
     *         the same as the width provided to
     *         {@link #newRange(long, long, long)}. This width will be 0 for a
     *         range from 5 to 5, i.e. one less.
     */
    public long getWindowWidth() {
        return distance(start);
    }

    /**
     * Creates a new Range that starts at the given number, and is the given
     * width, wrapping around to 0 at edge. (i.e. edge is the largest number in
     * the range). edge may not be Long.MAX_VALUE.
     * 
     * @param start
     * @param width
     * @param edge
     * @return a thusly initialized Range.
     */
    public static Range newRange(long start, long width, long edge) {
        if (edge == Long.MAX_VALUE) {
            throw new IllegalArgumentException("edge must be less than "
                    + Long.MAX_VALUE);
        }
        if (width < 1) {
            throw new IllegalArgumentException(
                    "A range must have a width of 1 or greater!");
        }
        // we include start, so we have to subtract 1 from width
        return new Range(start, (start + width - 1) % (edge + 1), edge);
    }

    public long getMin() {
        return start > end ? 0 : start;
    }

    public long getMax() {
        return start > end ? edge : end;
    }

    @Override
    public String toString() {
        return String.format("(%d..%d)[%d]", start, end, edge);
    }

}
